package com.bolt.report.business.modules.dataset.service;

import com.bolt.common.BoltConstants;
import com.bolt.common.collection.IteratorUtil;
import com.bolt.common.db.converts.IJdbcTypeConvert;
import com.bolt.common.db.converts.JdbcTypeConvertFactory;
import com.bolt.common.reflect.ClassUtil;
import com.bolt.common.utils.ApacheRequestUtil;
import com.bolt.common.utils.JacksonUtil;
import com.bolt.common.utils.SpringContextUtil;
import com.bolt.common.utils.StrUtil;
import com.bolt.convention.exception.ServiceException;
import com.bolt.report.business.modules.dataset.dto.ReportDataSetDTO;
import com.bolt.report.business.modules.dataset.dto.converter.ReportDataSetConverter;
import com.bolt.report.business.modules.dataset.entity.ReportDataSetEntity;
import com.bolt.report.business.modules.dataset.repository.ReportDataSetRepository;
import com.bolt.report.business.modules.datasource.dto.DataSetAnalyzeParamDTO;
import com.bolt.report.business.modules.datasource.dto.ReportDataSourceDTO;
import com.bolt.report.business.modules.datasource.dto.converter.ReportDataSourceConverter;
import com.bolt.report.business.modules.datasource.service.ReportDataSourceService;
import com.bolt.report.business.modules.field.dto.converter.ReportDbFieldConverter;
import com.bolt.report.business.modules.field.entity.ReportDbFieldEntity;
import com.bolt.report.business.modules.field.service.ReportDbFieldService;
import com.bolt.report.business.modules.param.dto.converter.ReportDbParamConverter;
import com.bolt.report.business.modules.param.entity.ReportDbParamEntity;
import com.bolt.report.business.modules.param.service.ReportDbParamService;
import com.bolt.report.config.ReportTokenService;
import com.bolt.report.definition.*;
import com.bolt.report.engine.ApiDataConvertAdapter;
import com.bolt.report.engine.DataProvider;
import com.bolt.report.engine.ObjectFactory;
import com.bolt.report.engine.pool.JdbcConnectionPool;
import com.bolt.report.exception.ReportBizExceptionEnum;
import com.bolt.support.base.service.BaseService;
import com.bolt.support.spring.jpa.query.PropertiesSqlParameterSource;
import com.bolt.support.spring.jpa.repository.BasicJpaRepository;
import com.bolt.support.sqlconfig.base.SqlStatement;
import com.bolt.support.sqlconfig.base.SqlText;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.springframework.beans.BeanUtils;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.*;

/**
 * Auto Generate Javadoc
 *
 * @author 日期:2021-11-17 14:15:01
 **/
@Service
@Transactional(readOnly = true, rollbackFor = Exception.class)
@RequiredArgsConstructor
@Slf4j
public class ReportDataSetService extends BaseService<ReportDataSetEntity, String> {

    private final ReportDataSetRepository reportDbRepository;

    @Override
    protected BasicJpaRepository<ReportDataSetEntity, String> getRepository() {
        return reportDbRepository;
    }

    private final ReportDbFieldService dbFieldService;

    private final ReportDbParamService dbParamService;

    private final ReportDataSourceService dataSourceService;

    @Resource(name = "druidJdbcPool")

    private JdbcConnectionPool connectionPool;

    @Transactional(rollbackFor = Exception.class)
    public ReportDataSetEntity saveOrUpdate(ReportDataSetDTO dto) {
        if (StrUtil.isEmpty(dto.getId())) {
            ReportDataSetEntity entity = ReportDataSetConverter.toEntity(dto);
            Set<ReportDbFieldEntity> fields = ReportDbFieldConverter.toEntity(dto.getFields());
            Set<ReportDbParamEntity> params = ReportDbParamConverter.toEntity(dto.getParams());
            entity.getParams().addAll(params);
            entity.getFields().addAll(fields);
            return save(entity);
        } else {
            ReportDataSetEntity entity = findOne(dto.getId()).orElseThrow(() -> new ServiceException(ReportBizExceptionEnum.DATA_NOT_FOUND));
            Set<ReportDbFieldEntity> oldfieldEntities = entity.getFields();
            Set<ReportDbParamEntity> oldparamEntities = entity.getParams();
            Set<ReportDbFieldEntity> fields = ReportDbFieldConverter.toEntity(dto.getFields());
            Set<ReportDbParamEntity> params = ReportDbParamConverter.toEntity(dto.getParams());

            if (IteratorUtil.isEmpty(fields) && IteratorUtil.isNotEmpty(oldfieldEntities)) {
                oldfieldEntities.clear();
            } else {
                oldfieldEntities.removeIf(r2 -> !fields.stream().anyMatch(id -> id.equals(r2.getId())));
                for (ReportDbFieldEntity field : fields) {
                    oldfieldEntities.add(field);
                }
            }
            if (IteratorUtil.isEmpty(oldparamEntities) && IteratorUtil.isNotEmpty(oldparamEntities)) {
                oldparamEntities.clear();
            } else {
                oldparamEntities.removeIf(r2 -> !params.stream().anyMatch(id -> id.equals(r2.getId())));
                for (ReportDbParamEntity paramEntity : params) {
                    oldparamEntities.add(paramEntity);
                }
            }
            ReportDataSetConverter.toEntity(entity, dto);
            return reportDbRepository.save(entity);
        }


    }

    public boolean validateCode(String code) {
        long number = findCount(k -> k.lambda().eq(ReportDataSetEntity::getDbCode, code).build());
        return number > 0;
    }

    public List<Map<String, Object>> analyze(DataSetAnalyzeParamDTO dto) {
        switch (dto.getType()) {
            case SQL:
                return executeRelationalDb(dto);
            case SPRING:
                return executeBean(dto);
            case API:
                return executeApi(dto);
            default:
                throw new ServiceException(ReportBizExceptionEnum.DATA_SOURCE_TYPE_NOT_SUPPORT);
        }
    }

    private List<Map<String, Object>> executeBean(DataSetAnalyzeParamDTO dto) {
        if (SpringContextUtil.containsBean(dto.getSpringKey())) {
            Class component = SpringContextUtil.getType(dto.getSpringKey());
            Method method = BeanUtils.findMethod(component, "loadData", Map.class);
            TypeInformation typeInformation = ClassTypeInformation.from(component);
            TypeInformation<?> returnType = typeInformation.getReturnType(method);
            TypeInformation<?> componentType = returnType.getComponentType();
            Class<?> clazz = componentType.getType();
            List<Map<String, Object>> list = new ArrayList<>();
            List<ParameterDefinition> paramDefinitions = ReportDbParamConverter.toDefinition(dto.getParams());
            if (Map.class.isAssignableFrom(clazz)) {
                DataProvider dataProvider = (DataProvider) SpringContextUtil.getBean(dto.getSpringKey());
                List<?> data = dataProvider.loadData(convertQueryParam(paramDefinitions));
                if (IteratorUtil.isEmpty(data)) {
                    throw new ServiceException(ReportBizExceptionEnum.SPRINGID_FIELD_ANALYZE_ERROR);
                }
                Map map = (Map) data.get(0);
                for (Iterator<String> it = map.keySet().iterator(); it.hasNext(); ) {
                    Map<String, Object> fields = new HashMap<>();
                    String key = it.next();
                    Object value = map.get(key);
                    fields.put("fieldName", key);
                    fields.put("fieldExpr", key);
                    fields.put("fieldText", key);
                    fields.put("fieldType", value != null ? value.getClass().getSimpleName().toUpperCase() : value);
                    fields.put("orderType", -1);
                    list.add(fields);
                }
            } else {
                PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(clazz);
                for (PropertyDescriptor property : propertyDescriptors) {
                    Map<String, Object> fields = new HashMap<>();
                    fields.put("fieldName", property.getName());
                    fields.put("fieldExpr", property.getName());
                    fields.put("fieldText", property.getName());
                    fields.put("fieldType", property.getPropertyType().getSimpleName().toUpperCase());
                    fields.put("orderType", -1);
                    list.add(fields);
                }
            }
            return list;
        } else {
            throw new ServiceException(ReportBizExceptionEnum.SPRINGID_NOT_FOUND);
        }
    }

    private List<Map<String, Object>> executeRelationalDb(DataSetAnalyzeParamDTO dto) {
        ReportDataSourceDTO sourceDTO = dataSourceService.findDataSource(dto.getDataSourceId());
        JdbcDataSourceDefinition dataSourceDefinition = ReportDataSourceConverter.toDataSourceDefinition(sourceDTO);
        Connection pooledConnection = null;
        try {
            pooledConnection = connectionPool.getPooledConnection(dataSourceDefinition);
            SqlStatement sqlStatement = ObjectFactory.parseSqlText(dto.getDbDynSql());
            List<ParameterDefinition> parameterDefinitions = ReportDbParamConverter.toDefinition(dto.getParams());
            Map<String, Object> params = convertSqlQueryParam(parameterDefinitions);
            SqlText sqlText = sqlStatement.getSqlText(params);

            SingleConnectionDataSource datasource = new SingleConnectionDataSource(pooledConnection, true);
            NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(datasource);
            SqlParameterSource parameterSource = new PropertiesSqlParameterSource(sqlText.getParameterValues());

            IJdbcTypeConvert typeConvert = JdbcTypeConvertFactory.getJdbcTypeConvert(sourceDTO.getDbUrl());
            List<Map<String, Object>> filedInfo = jdbcTemplate.query(sqlText.getSql(), parameterSource, (ResultSetExtractor<List<Map<String, Object>>>) rs -> {
                        List<Map<String, Object>> list = new ArrayList<>();
                        ResultSetMetaData metaData = rs.getMetaData();
                        int columnCount = metaData.getColumnCount();
                        for (int i = 1; i <= columnCount; i++) {
                            Map<String, Object> data = new HashMap<>();
                            String columnName = metaData.getColumnLabel(i);
                            String columnType = metaData.getColumnTypeName(i);
                            data.put("fieldName", columnName);
                            data.put("fieldText", columnName);
                            data.put("fieldExpr", columnName);
                            data.put("fieldType", typeConvert.processTypeConvert(columnType));
                            data.put("orderType", -1);
                            list.add(data);
                        }
                        return list;
                    }
            );
            return filedInfo;
        } catch (Exception throwable) {
            log.error("error", throwable);
            throw new ServiceException(ReportBizExceptionEnum.EXECUTE_SQL_ERROR, throwable);
        } finally {
            try {
                if (pooledConnection != null) {
                    pooledConnection.close();
                }
            } catch (SQLException throwable) {
                log.error("error", throwable);
                throw new ServiceException(ReportBizExceptionEnum.DATA_SOURCE_CONNECTION_FAILED, throwable);
            }
        }
    }

    private Header getToken() {
        ReportTokenService tokenService = ObjectFactory.getTokenService();
        String token = tokenService.getToken();
        if (StrUtil.isNotBlank(token)) {
            Header header = new BasicHeader(BoltConstants.AUTH_HEADER, token);
            return header;
        }
        return null;
    }

    private List<Map<String, Object>> executeApi(DataSetAnalyzeParamDTO dto) {
        try {
            if (StrUtil.isEmpty(dto.getApiUrl())) {
                throw new ServiceException(ReportBizExceptionEnum.API_URL_EMPRY);
            }
            String context = null;
            List<ParameterDefinition> paramDefinitions = ReportDbParamConverter.toDefinition(dto.getParams());
            switch (dto.getApiMethod()) {
                case GET:
                    context = ApacheRequestUtil.get(dto.getApiUrl(), getToken(), convertQueryParam(paramDefinitions));
                    break;
                case POST:
                    if (dto.getRequestType() == RequestType.FORMDATA) {
                        context = ApacheRequestUtil.post(dto.getApiUrl(), getToken(), convertQueryParam(paramDefinitions));
                    } else {
                        context = ApacheRequestUtil.post(dto.getApiUrl(), getToken(), JacksonUtil.toJSONString(dto.getParams()));
                    }
                    break;
                default:
                    context = ApacheRequestUtil.get(dto.getApiUrl(), getToken(), convertQueryParam(paramDefinitions));
            }
            if (StrUtil.isEmpty(context)) {
                throw new ServiceException(ReportBizExceptionEnum.RESPONSE_CONTENT_EMPTY);
            }

            JsonNode jsonObject = JacksonUtil.getObjectMapper().readTree(context);
            if (!jsonObject.has("data") && StrUtil.isEmpty(dto.getConvertAdapter())) {
                throw new ServiceException(ReportBizExceptionEnum.RESPONSE_JSON_FORMART_ERROR);
            }
            if (!jsonObject.has("data") && StrUtil.isNotEmpty(dto.getConvertAdapter())) {
                Object convertAdapter = ClassUtil.newInstance(dto.getConvertAdapter());
                if (convertAdapter instanceof ApiDataConvertAdapter) {
                    String newContext = ((ApiDataConvertAdapter) convertAdapter).convert(context);
                    jsonObject = JacksonUtil.getObjectMapper().readTree(newContext);
                }
            }
            JsonNode dataNode = jsonObject.get("data");
            List<Map<String, Object>> list = new ArrayList<>();
            JsonNode oneNode = null;
            if (dataNode.isArray()) {
                oneNode = ((ArrayNode) dataNode).get(0);
            } else {
                oneNode = dataNode;
            }
            for (Iterator<String> it = oneNode.fieldNames(); it.hasNext(); ) {
                String name = it.next();
                Map<String, Object> fields = new HashMap<>();
                fields.put("fieldName", name);
                fields.put("fieldExpr", name);
                fields.put("fieldText", name);
                fields.put("fieldType", String.class.getSimpleName().toUpperCase());
                fields.put("orderType", -1);
                list.add(fields);
            }
            return list;
        } catch (Exception ex) {
            log.error("error", ex);
            throw new ServiceException(ex);
        }

    }


    public DataSetDefinition findDataSourceDefinition(String dataSetId) {
        ReportDataSetEntity reportDataSetEntity = findOne(dataSetId)
                .orElseThrow(() -> new ServiceException(ReportBizExceptionEnum.DATA_NOT_FOUND));
        DataSetDefinition dataSetDefinition = ReportDataSetConverter.toDataSetDefinition(reportDataSetEntity);
        if (reportDataSetEntity.getDbType() == 0) {
            ReportDataSourceDTO sourceDTO = dataSourceService.findDataSource(reportDataSetEntity.getDbSourceId());
            JdbcDataSourceDefinition dataSourceDefinition = ReportDataSourceConverter.toDataSourceDefinition(sourceDTO);
            dataSetDefinition.setJdbcConnectionDef(dataSourceDefinition);
        }
        return dataSetDefinition;
    }

    private Map<String, Object> convertSqlQueryParam(List<ParameterDefinition> definitions) {
        Map<String, Object> param = new HashMap<>();
        for (ParameterDefinition definition : definitions) {
            String paramName = definition.getName();
            ParameterType paramType = definition.getParameterType();
            if (StrUtil.isNotBlank(definition.getDefaultValue())) {
                Object value = ObjectFactory.convertParameter(definition.getDefaultValue(), paramType);
                param.put(paramName, value);
            }

        }
        return param;
    }


    public Map<String, Object> convertQueryParam(List<ParameterDefinition> definitions) {
        Map<String, Object> param = new HashMap<>();
        if (IteratorUtil.isEmpty(definitions)) {
            return param;
        }
        for (ParameterDefinition definition : definitions) {
            String paramName = definition.getName();
            if (StrUtil.isNotBlank(definition.getDefaultValue())) {
                param.put(paramName, definition.getDefaultValue());
            }
        }
        return param;
    }
}
